Github
PostsnetworkAuthentication and Authorization

Authentication and Authorization

인증과 인가

인증과 인가란

인증은 사용자나 기기의 신원을 확인하고 권한을 주는 것을 말한다. 사용자의 ID, Password를 통해서 로그인을 하게되면 서버에 저장되어있는 사용자인지 확인하는 것이 하나의 예로 들 수 있다.

인가란 접근 가능한지 권한을 확인하는 것을 말한다. 토큰을 사용해서 권한을 확인하는 방식을 예로 들 수 있다.

체크메이트라는 프로젝트를 진행하면서 인증/인가를 적용했다. 체크메이트는 모임의 출석과 출결을 관리해주는 서비스로 자신의 출석을 체크하고, 다른 사람의 출결을 관리하기 위해서는 인증/인가는 반드시 적용해야했다. 이번 글에서는 체크메이트를 하면서 인증과 인가를 어디에서 사용했고, 어떻게 적용했는지 말할려고 한다.


인증
  1. 로그인 먼저 자체 로그인 방식은 익숙한 방식으로 회원가입을 통해서 등록한 사용자의 ID와 비밀번호를 입력받아서 서버의 저장된 데이터와 비교하는 과정을 통해서 구현했다.

    이 과정에서 중요했던 점은 비밀번호를 어떻게 관리하냐는 것이었다. 비밀번호는 보안성을 높이는 것이 중요했기 때문에 서버에서는 평문으로 저장하는 것이 아니라 암호화한 뒤에 저장했다. sha3-256 암고리즘을 사용해서 암호화했다. 비밀번호를 전달받고 해싱 함수를 통해서 해싱한 값을 저장되어 있는 값과 비교해서 일치하면 인증 과정을 마치게 된다.

    해싱 알고리즘에는 MD5, HAS-160, SHA 시리즈가 있다. 그 중에서 SHA는 미국 국가안보국에서 만들어서 그런가 보안이 뛰어나서 많이 사용하는 것 같다. 해싱 알고리즘은 해싱 함수를 통해서 암호화시키는 방식으로 입력값에 대한 항상 같은 값을 리턴한다. 그렇기 때문에 무한정으로 대입하게 되면 결국에는 대입 가능한 값을 찾게 되는데 최신에 들어서 GPU 등 성능이 개선되면서 연산 속도가 빠르기 때문에 SHA-1, SHA-2의 보안성이 약화되는 시점에서 SHA-3를 통해서 더욱 보안성이 높은 해싱 알고리즘이 사용된다.

    또 다른 보안 문제는 비밀 번호를 어떻게 서버로 전송하냐는 것이다. 비밀 번호를 전송하는 과정에서 탈취를 당할 가능성이 있고, 탈취를 당했을 때를 대비해하는 점이 있다는 것을 알게 되었다. 기존에 https를 통해서 보안성을 높이고 있었다. https는 SSL 프로토콜의 인증 방식을 사용하게 되는데 다음의 그림과 같은 과정을 거친다.

    https를 사용하게 되면 안전하게 대칭키를 전달할 수 있고, 대칭키를 통해서 HTTP 요청과 응답 데이터를 암호화할 수 있기 때문에 보안성을 높일 수 있다.

    이 정도면 보안적으로 충분하지 않을까 생각했는데, 비밀번호를 body에 평문으로 전송하냐는 질문을 받으면서 https로는 안전하지 않은건가 생각하게 되었다. 다른 실서비스와 같은 경우에 https를 사용하지만 비밀번호를 한 번더 암호화를 한다는 것을 알게 되었다. 추가적인 암호화를 통해서 보안을 더욱 높이는 것인데 비즈니스를 하는 실서비스와 같은 경우에는 매우 중요한 정보같은 경우에 노출되면 타격이 크기 때문이라고 생각한다. 하지만 모든 데이터에 암호화를 하지 않는 이유처럼 상황에 따라 사용하면 된다고 생각한다.


  2. 소셜 로그인 체크메이트에서는 2가지의 로그인 방식을 제공하기로 했다. 처음에는 서비스 자체 로그인 방식만을 운영했지만 회원가입을 하는 과정에서 번거롭다는 피드백을 받기도 했고, 과정에서 이탈하는 사용자를 직접 목격했기 때문에 후에 Google 소셜 로그인 을 추가했다.

    OAuth2.0 방식을 사용해서 google에 등록된 사용자의 정보를 얻어왔다. 구현한 방식은 다음과 같다.

    프론트엔드

    1. User를 client_id, redirect_uri, response_type, scope를 파라미터로 추가한 google auth url로 리다이렉트한다.
    2. User가 권한에 동의하면 Google에서 등록한 redirect url(체크메이트)에 code 파라미터로 리다이렉트한다.
    3. 해당 code를 백엔드에 보내며 Login 요청을 한다.
    4. 백엔드에서 응답해준 CheckMate 자체 Access Token을 이용한다.

    백엔드

    1. 프론트에게 구글 로그인을 요청받는다(code를 받음)
    2. 해당 코드를 이용해서 Google에게 사용자 정보가 담긴 Token(ID 토큰)을 요청한다.
    3. ID Token을 디코딩해서 User 정보를 얻는다.
    4. 해당 User가 CheckMate의 회원인지 검사한다.
    5. 이미 등록된 회원이면 (자체)Access Token을 응답해주고 새로운 회원이면 DB등록 후에 (자체)Access Token을 응답한다.

    OAuth 방식은 복잡한만큼 안전하지만 몇가지 보안적인 취약점이 있다. 대표적으로 CSRF를 통한 공격인데 redirect로 code를 받고, Client 서버로 전송하는 내용을 추출하여 만든 CSRF 공격 페이지에 사용자가 접근하면, 사용자 계정과 공격자 계정이 연동되며, 공격자의 SNS 계정을 통해 사용자 계정으로 로그인을 할 수 있다고 한다. 이 부분은 code를 서버로 전송할 때, CSRF 토큰을 같이 전송하고 검증과정을 철저하게 한다면 해결할 수 있다고 한다.

    두번째로 redirect url에 대한 검증이 미흡할 경우 발생할 수 있는 문제점인데 공격자는 사용자에게 변조된 Redirect URI를 보내 로그인을 유도한다. 사용자가 Redirect URI 값이 변조된 URL로 로그인할 경우, Authorization code 값이 공격자 서버로 전달되어 공격자는 사용자의 계정을 탈취할 수 있다. 이 부분은 redirect url을 google cloud에 설정해놓음으로써 인증과정에서 redirect url을 검사하는 것으로 해결할 수 있다고 생각한다.

    소셜 로그인 방식을 도입하고 나서 확인해본 결과, 사용자의 80% 이상이 소셜 로그인 방식으로 가입을 했다 👍


  3. 이메일 인증 기존의 체크메이트에서는 아이디를 이메일 형식으로 사용하고 있었다. 보편적으로 이메일을 사용하는 이유는 사용자의 이메일로 이벤트를 전송하거나 정보를 전달하기 위해서라고 생각한다. 체크메이트에서는 이메일로 전송하지는 않지만 모임 초대 링크를 보낼 수도 있고, 충분히 사용할 수 있는 방향이 많기 때문에 이메일을 인증하는 과정이 필요했다.

    처음 생각했던 이메일 인증 구현하는 방식을 2가지가 있었다. 인증 코드를 통한 로그인 방식과 메일로 전달된 링크를 통한 방식이 있었다. 실서비스에서도 두 방식이 다 사용되서 고민이 되었다.

    결론적으로 인증 코드 방식을 사용하게 되었다. 이유는 콜백 링크같은 경우에는 이전 페이지를 포기하고 새로운 브라우저 탭을 연다는 점이 불편하다는 의견이 있었고, 일부 사용사들은 이메일로 전달된 링크를 클릭하는 것에 부정적인라는 점이 있었다. 인증 코드 방식은 다음과 같이 구현했다.

    1. 입력한 이메일을 서버로 전송한다.
    2. 서버는 이메일이 저장되어있다면 중복된 이메일이라는 응답을 보낸다.
    3. 서버는 체크메이트 구글 계정으로 해당 이메일에 인증코드를 전송을 구글 서버로 요청한다.
    4. 사용자에게 입력받은 코드를 서버로 전송한다.

    기능을 구현하고 나서 소셜 로그인으로 가입한 유저의 이메일과 회원가입을 통해서 가입한 이메일을 매칭했다. 이전에 구글 이메일로 가입한 사용자라면 OAuth방식으로 가입한 이메일과 일치했을 때 매칭되도록 했다.


인가

인증, 인가 방식에는 JWT 방식과 Cookie & Session 방식 등을 사용할 수 있다.

JWT(JSON Web Token)
https://www.rfc-editor.org/rfc/rfc7519에 명시된 방식으로 JSON 형식으로 정보를 저장하는 방식이다. Json 형식의 Header, Payload, Signature 3부분을 인코딩하여 .으로 이어붙인다.

Header에는 토큰의 타입, 알고리즘 방식 저장한다. Payload에 정보를 저장하게 되는데 이 정보의 조각들을 클레임(claim)이라고 부른다. 클레임의 종류에는 registered claim, public claim, private claim이 있다. register claim은 이미 정해진 종류의 데이터로 토큰의 발급자, 제목, 만료 시간등이 저장된다. public claim은 공개용 정보를 저장하며 private claim은 클라이언트와 서버 사이에 임의로 필요한 정보를 저장한다.

JWT의 장점은 첫번째로 토큰 자체에 정보를 저장하기 때문에 별도의 저장소가 필요없다는 점이다. 두번째로 매 요청마다 ID를 조회해야하는 세션을 통한 방식과 달리 서버측 부하를 낮출 수 있다. 세번째로 토큰 자체에 데이터가 들어있어 클라이언트에서 받아 위조되었는지만 판별하면 된다.

단점은 첫번째로 담는 정보가 많아질 수록 크기가 커진다는 점이다. 크기가 커지면 header에 담을 때 크기를 주의해야한다. 두번째로 암호화를 하는 것이 아니라 단순히 인코딩하는 것이기 때문에 탈취당하면 정보를 볼 수 있다. 세번째로 토큰을 클라이언트에서 저장해야 하기 때문에 보안적으로 문제가 될 수 있다.

Cookie & Session
서버측 Session에 정보를 저장하고 해당하는 Id를 Cookie에 저장하는 방식이다. Set-Cookie를 통해서 클라이언트 쿠키에 정보가 저장되며, 요청 시에 Credential을 추가해서 브라우저가 response에 대해 동작을 하도록 허용 여부를 설정해야한다.

세션 기반 인증은 서버가 파일이나 데이터베이스 세선정보를 가지고 있어야 하고, 이를 조회하는 과정이 필요해 많은 인증에서만 많은 오버헤드가 발생한다. 하지만, 토큰은 세션과 달리 서버가 아닌 클라이언트에 저장되기 때문에 메모리나 스토리지 등을 통해 세션을 관리하는 서버의 부담을 덜 수 있다.

accessToken & refreshToken

ATK를 권한이 필요한 요청마다 http 메세지 header 필드에 추가해서 서버에 보낸다. 서버에서는 ATK에 포함된 사용자의 정보를 확인하고 리소스에 접근할 권한이 있는지 확인한다.

ATK를 클라이언트에 저장해서 사용하면 탈취될 가능성이 있다. 이를 극복하고자 access token의 사용 기간을 짧게 가져가는 방식이 있다. 하지만, 그렇게되면 사용자는 매번 짧은 주기마다 로그인을 행해야 하고, 체크메이트의 서비스 특성상 로그인되지 않은 사용자는 그 어떤 기능을 사용할 수 없기 때문에 로그인을 유지시켜주어야 한다. 때문에 RTK를 도입했다.

RTK를 이용하면 ATK가 만료되었더라도 토큰 재발급을 통해 사용자에게 긴 로그인 시간을 제공할 수 있다.

하지만 여기서도 동일하게 발생하는 문제는 RTK도 탈취가 가능하다는 점이다. Refresh Token마저 탈취당하게 되면 해커는 RTK를 통해 재발급도, ATK를 이용해 보호된 자원까지 이용할 수 있게 된다. 토큰이 유출되었다는 것을 인지하게 되면 token을 무효화시켜야 하는데, JWT 방식은 서버에서 그 어떤 상태도 저장하지 않고 있기 때문에 불가능하다.

이를 해결하기 위해서 짧은 생명 주기를 가진 ATK와 RTK을 이용한다. 이때, RTR(Refresh Token Rotation) 방법으로 RTK가 사용될 때마다 새로운 ATK과 RTK을 발급한다. 이렇게 되면, 토큰의 무효화도 짧은 주기로 이루어지기 때문에 몇 분의 시간이 지나고 나면 토큰이 무효화된다.

토큰을 어디에 저장해서 사용해야하는지 고민이 되었다. 브라우저에 저장하게되면 보안적으로 문제가 발생할 수 있기 때문이다. 대표적으로 XSS, CSRF가 있다. 이 두개만 신경쓰면 안전한 것은 아니지만 당장은 2가지 방식을 가장 크게 고려했다.

XSS는 악의적인 JS코드를 실행시키는 방식이다. 예시로 게시판에 글을 열어볼때 script를 실행시켜서 공격자의 주소로 데이터를 보내거나 입력값을 검증하지 않는 input일 경우에 악성 코드를 입력해서 주입할 수 있다. localStorage를 사용하는 경우에는 XSS 공격에 취약하다. js로 localStorage에 값을 꺼내오기는 코드 한 줄이면 가능하기 때문에 굉장히 쉽다. Cookie 같은 경우에는 httpOnly 옵션을 추가하게 되면 js로 접근할 수 없기 때문에 xss 공격에는 안전할 수 있다. 하지만 서버 요청과 함께 전송되기 때문에 사용자의 컴퓨터를 위조하게되면 위험하다.

CSRF는 권한이 있는 사용자를 이용하는 방식이다. 공격자는 사용자가 웹 페이지 방문, 링크 클릭 등과 같은 작업을 수행하도록 유도하여 사용자를 대신하여 웹 사이트에 HTTP 요청을 보낸다. 사용자가 신뢰할 수 있는 웹 사이트에 활성 인증된 세션을 가지고 있는 경우 요청은 사용자가 보낸 요청으로 처리된다. localStorage를 사용하는 경우에는 ATK를 요청 header에 담아서 보내기 때문에 CSRF에는 안전한편이지만 Cookie는 요청과 함께 전송되기 때문에 취약할 수 있다.

localStorage에 저장하게 되면 XSS 공격에 취약하지만 리액트를 사용함으로써 일정 부분 막을 수 있다. React DOM은 JSX에 삽입된 모든 값을 렌더링하기 전에 이스케이프한다. &&amp; 또는 <&lt;로 변환된다. 렌더링이 되기 전에 변환된 값들이 브라우저에선 입력한 그대로 보이게 되지만, HTML 태그나 스크립트 기능이 제거되기 때문에 XSS 공격을 막을 수 있다. 하지만 완전히 막을 수 없고 추가적인 설정이 필요하다고 하니 이 부분을 알게되는데로 포스팅 해야겠다.

결과적으로 ATK를 in memory로 저장하게 되었는데 그 이유는 헤더에 실어서 보내기 때문에 CSRF는 신경쓰지 않았고, XSS 공격같은 경우에 저장된 값을 볼 수 있는 가능성이 매우 적기 때문이다. 하지만 구현하기 까다롭다는 점이 있었다. 메모리에 저장하게 되면 어플리케이션을 재실행시켰을 때 값이 없어지기 때문에 맨 처음 ATK를 요청해서 가져와야한다. 이 부분을 RTK를 통해서 ATK를 가져오는 방식을 해결했다. RTK로 ATK를 요청해서 가져오고 나서 다음 동작을 예측할 수 없기 때문에 보안적인 측면에서도 문제가 되지 않을 것라고 생각한다. 리액트를 같이 사용하면서 ATK를 상태로 저장했고, interceptor를 통해서 매 요청마다 header에 ATK를 추가하거나 ATK를 지우기 위해서 리액트에서 Interceptor 객체로 주입해주는 과정이 복잡했다. 또한 페이지가 렌더링 되자마자 요청되야하는 작업들이 있는데 그 전에 ATK를 확인해야하는 과정이 추가적으로 들어가는 점도 복잡성을 높였다고 생각한다.

RTK는 쿠키를 사용했다. 세션을 포함하지 않는 이유는 서버에서 임의값을 생성해서 Redis의 키로 사용하고 정보를 저장한 다음, 해당 키값을 쿠키로 내려준다.